/**
 * layui表单管理
 *
 * HISTORY:
 *
 *    @Author: Malphite
 *    @Date: 2022-11
 *    @description:
 *
 *      **********    组件重构  v2.0.0    **************
 *    layui.v = 2.5.6
 *
 *    问题1: '' 和 0 和 '0' 不能识别出区别
 *
 *      由于属性值都是字符串，但是对象值可能是数字，为了保证数字和它对相应的字符串可以匹配
 *    所以这里使用 == 。 但是由于 '' == 0  所以切换 0 和 '' 项时会出现问题。
 *
 *    问题2： 建议加上表单的lay-filter属性值
 *
 *    问题3： 对于checkbox最后获取值稍有不同
 *
 *    问题4:  原有的表单校验没有检验到单选或者多选框为空的情形
 *
 *    问题5: 建议还是使用方法获取最终的表单值， 变量form里面的值可以自有修改可能有错误的值
 *
 *    问题6: 在属性修改之后还是会调用render方法
 *
 *    问题7： 修改了render方法的返回值
 *
 */
 ("use strict");

 layui.define(["binder", "form"], function (exports) {
   const renderForever = layui.form.render;
 
   /**
    * 定义一系列的公共常量
    */
   let constant = {
     /**
      * @var {*} 当前版本号
      */
     VERSION: "2.0.0",
 
     /**
      * @var {*} 全局计数
      */
     INTERNAL_INDEX: 0,
 
     /**
      * @var {String} 字符串分隔符 -> .
      * @description
      *    主要用于拆分属性key的时候使用
      */
     separator: ".",
 
     /**
      * @var {String} layui里面过滤属性的名称
      */
     LAYUI_FILTER: "lay-filter",
 
     /**
      * @var {String} layui里面表单标志class名称
      */
     LAYUI_FORM: "layui-form",
 
     /**
      * @var {String} 数据放入表单里面的form位置
      */
     LAYUI_FORM_STR: "form",
 
     /**
      * @var {String} layui表单验证 是否懒校验的属性值
      * @desc
      *    不设置这个属性 input采用 input onporpertychange 事件监听
      *    设置这个属性值 input采用 onbulr 事件监听
      *    当这个值设置为true的时候，属性值修改时不进行校验，在表单提交时通过方法一起校验
      *    当这个值设置为false or 其它时，在属性值发生改变时校验
      */
     LAYUI_LAZY: "lay-lazy",
 
     /**
      * @var {String} layui表单验证的属性值
      */
     LAYUI_VERIFY: "lay-verify",
 
     /**
      * @var {String} layui表单形式的属性值
      */
     LAYUI_VERIFY_TYPE: "lay-verType",
 
     /**
      * @var {String} layui表单验证提示信息的属性值
      */
     LAYUI_VERIFY_TEXT: "lay-reqText",
 
     /**
      * @var {String} layui表单验证失败提醒的class
      */
     LAYUI_DANGER: "layui-form-danger",
   };
 
   /**
    * 工具方法集合
    */
   var util = {
     /**
      * @method 获取表单元素的lay-filter属性值
      * @param {*} formItem
      */
     getFormFilterName(formItem) {
       let filter = formItem.getAttribute(constant.LAYUI_FILTER);
       if (filter) return filter;
       filter = "layui-form-vform-" + formItem.name + '&' + constant.INTERNAL_INDEX++;
       formItem.setAttribute(constant.LAYUI_FILTER, filter);
       return filter;
     },
 
     /**
      * @method 获取当前form表单的name,就是它的lay-filter属性值，如果没有就生成一个
      * @param {*} item 表单的dom
      * @return 表单名称
      */
     getFormName: function (item) {
       let name = item.getAttribute(constant.LAYUI_FILTER);
       if (!name) {
         name = "layui-form-vform" + constant.INTERNAL_INDEX++;
         item.setAttribute(constant.LAYUI_FILTER, name);
       }
       return name;
     },
 
     /**
      * @method 通过名称在form里面寻找合适的视图
      * @param {*} name form的名称
      */
     getFormByName: function (name) {
       let res = null;
       _.every(layui.form.$children, (c) => {
         // 移除已经失效的
         if (!document.contains(c.$ele)) {
           c.$destroy();
           if (layui.form.$children.indexOf(c) >= 0)
             layui.form.$children.splice(layui.form.$children.indexOf(c), 1);
           return;
         }
 
         if (c.$name == name) {
           res = c;
           return false;
         }
       });
       return res;
     },
 
     /**
      * @method 遍历子节点并执行回调函数
      * @param {*} children
      * @param {*} cb
      * @returns
      */
     findChildren: function (children, cb) {
       if (children.length == 0) return;
       _.each(children, (child) => {
         cb && cb(child);
         util.findChildren(child.children, cb);
       });
     },
 
     /**
      * @method 获取虚拟dom里面对应属性的值
      * @desc
      *    这个是从binder.js里面复制出来的，后面考虑放入vDom对象里面
      * @param {*} vDom  虚拟dom
      * @param {*} key   属性的key
      * @returns
      */
     getVDomAttr(vDom, key) {
       if (!vDom.attrs[key]) return "";
       return vDom.attrs[key].expression || "";
     },
   };
 
   /**
    * @namespace FormVerify
    * @desc 表单取值与校验相关
    */
   var FormVerify = {
     /**
      * 检查判断一个表单元素当前的值是否正常 from  layui.form
      * @param {*} item
      * @returns
      */
     verifyItem(item) {
       // 表单校验基于layui的form模块，没有就返回true 跳过校验
       if (!layui || !layui.form) return true;
       // layui的form模块是需要使用jQuery的，所以这里暂时可以使用$
       let othis = $(item);
       let parent = othis.parents().filter(".layui-form");
       let filter = parent.attr("lay-filter");
       let verifys = [];
       const defaultText = "请完成表单!";
       // 获取校验规则
       if (othis.attr(constant.LAYUI_VERIFY))
         verifys = othis.attr(constant.LAYUI_VERIFY).split("|");
       // 单选框和多选框的非空校验
       const checkflag =
         ("radio" === item.type || "checkbox" === item.type) &&
         verifys.length > 0 &&
         verifys[0] == "required";
       if (checkflag) {
         let index = 0;
         parent.find('[name="' + item.name + '"]').each(function () {
           if ($(this).get(0).checked === true) index++;
         });
         if ("checkbox" === item.type && index > 0) return true;
         if ("radio" === item.type && index === 1) return true;
         layui.layer.msg(othis.attr(constant.LAYUI_VERIFY_TEXT) || defaultText, {
           icon: 5,
         });
         return false;
       }
       // 剩下的就只有下拉框和输入框了
       let verifyType = othis.attr(constant.LAYUI_VERIFY_TYPE);
       let value = othis.val();
       let verify = layui.form.config.verify;
       let flag = true;
       _.every(verifys, function (v) {
         let isTrue = true,
           errorText = "",
           isFunction = _.isFunction(verify[v]);
         if (verify[v]) {
           isTrue = isFunction
             ? (errorText = verify[v](value, item))
             : !verify[v][0].test(value);
           errorText = errorText || verify[v][1];
           if (v === "required")
             errorText = othis.attr(constant.LAYUI_VERIFY_TEXT) || errorText;
           if (isTrue) {
             let parentNode =
               othis.parents().filter(".layui-form-scrollNode").length == 1
                 ? othis.parents().filter(".layui-form-scrollNode")
                 : othis.parents().filter(".layui-layer-content");
             let scrollFlag =
               parentNode.length == 1 &&
               (parentNode.get(0).scrollHeight >
                 parentNode.get(0).clientHeight ||
                 parentNode.get(0).offsetHeight >
                   parentNode.get(0).clientHeight);
             let proxyOthis =
               othis.get(0).tagName.toLowerCase() === "select" ||
               /^checkbox|radio$/.test(othis.get(0).type)
                 ? othis.next()
                 : othis;
             if (verifyType === "tips") {
               !scrollFlag &&
                 proxyOthis.get(0).getBoundingClientRect().height > 0 &&
                 layui.layer.tips(
                   errorText,
                   (function () {
                     if (typeof othis.attr("lay-ignore") !== "string") {
                       if (
                         othis.get(0).tagName.toLowerCase() === "select" ||
                         /^checkbox|radio$/.test(othis.get(0).type)
                       ) {
                         return othis.next();
                       }
                     }
                     return othis;
                   })(),
                   { tips: 1, tipsMore: true }
                 );
             } else if (verifyType === "alert") {
               layui.layer.alert(errorText, { title: "提示", shadeClose: true });
             } else {
               layui.layer.msg(errorText, { icon: 5, shift: 6 });
             }
             scrollFlag &&
               parentNode.animate(
                 {
                   scrollTop:
                     othis.offset().top -
                     parentNode.offset().top +
                     parentNode.scrollTop(),
                 },
                 800,
                 function () {
                   verifyType === "tips" &&
                     proxyOthis.get(0).getBoundingClientRect().height > 0 &&
                     layui.layer.tips(
                       errorText,
                       (function () {
                         if (typeof othis.attr("lay-ignore") !== "string") {
                           if (
                             othis.get(0).tagName.toLowerCase() === "select" ||
                             /^checkbox|radio$/.test(othis.get(0).type)
                           ) {
                             return othis.next();
                           }
                         }
                         return othis;
                       })(),
                       { tips: 1, tipsMore: true }
                     );
                 }
               );
             if (!layui.device().android && !layui.device().ios) {
               setTimeout(function () {
                 if (debounceFocus()) {
                   othis.get(0).focus();
                 }
               }, 7);
             }
             othis.addClass(LAYUI_DANGER);
             flag = !isTrue;
           }
         }
         return flag;
       });
       return flag;
     },
 
     /**
      * 检查一个表单里面的表单元素当前的值是否正常 from  layui.form
      * @param {*} filter
      * @returns
      */
     verifyForm(filter) {
       // 表单校验基于layui的form模块，没有就返回true 跳过校验
       if (!layui || !layui.form) return true;
       // layui的form模块是需要使用jQuery的，所以这里暂时可以使用$
       // 寻找parent 如果不传filter，这里需要将this指向VM
       let parent = filter ? $('[lay-filter="' + filter + '"]') : this.parent;
       // 获取表单dom
       let formItem = parent.hasClass("layui-form")
         ? parent
         : parent.find(".layui-form");
       // 获取表单元素集合
       let fieldElem = formItem.find("input,select,textarea");
       let flag = true;
       _.every(fieldElem, function (item) {
         if (item instanceof HTMLElement) {
           flag = FormVerify.verifyItem(item);
           return flag;
         }
       });
       return flag;
     },
 
     /**
      * 校验并获取一个表单的当前值 from  layui.form
      * @param {*} filter
      * @returns
      */
     getFormValue(filter) {
       // 表单校验基于layui的form模块，没有就返回空
       if (!layui || !layui.form) return null;
       // layui的form模块是需要使用jQuery的，所以这里暂时可以使用$
       // 校验,校验如果不通过就返回空
       if (!FormVerify.verifyForm.call(this, filter)) return null;
       // 寻找parent 如果不传filter，这里需要将this指向VM
       let parent = filter ? $('[lay-filter="' + filter + '"]') : this.parent;
       // 获取表单dom
       let formItem = parent.hasClass("layui-form")
         ? parent
         : parent.find(".layui-form");
       // 获取表单元素集合,这里是照搬form里面的 getValue 方法
       let fieldElem = formItem.find("input,select,textarea");
       let nameIndex = {};
       let field = {};
       layui.each(fieldElem, function (_, item) {
         item.name = (item.name || "").replace(/^\s*|\s*&/, "");
         if (!item.name) return;
         //用于支持数组 name
         if (/^.*\[\]$/.test(item.name)) {
           var key = item.name.match(/^(.*)\[\]$/g)[0];
           nameIndex[key] = nameIndex[key] | 0;
           item.name = item.name.replace(
             /^(.*)\[\]$/,
             "$1[" + nameIndex[key]++ + "]"
           );
         }
         if (/^checkbox|radio$/.test(item.type) && !item.checked) return;
         field[item.name] = item.value;
       });
       return field;
     },
   };
 
   /**
    * @namespace FormVerify
    * @desc 表单渲染相关
    */
   var FormRender = {
     /**
      * 更新表单元素的值
      * @param {*} vDom        虚拟节点
      * @param {*} expression  属性表达式
      * @param {*} value       需要更新的值
      * @param {*} vm          vm视图对象
      */
     updateData(vDom, expression, value, vm) {
       if (vDom.mark) {
         vm.proxy.getMarkValue.call(vm, expression, value, vDom.mark, vDom.arg);
       } else {
         vm.proxy.getValue.call(vm, expression, value);
       }
     },
 
     /**
      * 复选框的指令绑定渲染
      * @param {*} ele
      * @param {*} binding
      * @returns
      */
     renderCheckBox(ele, binding) {
       // 1. 复选框的当前value,和传入需要修改为的value
       let value = ele.value;
       let data = binding.value;
       // 判断复选框的当前value有没有在传入的取值范围内,如果有就选中如果没有就取消选中。
       ele.checked = _.indexOf(data, value) >= 0;
       // 判断是否需要检查当前的这个表单元素
       if (false !== ele.getAttribute(constant.LAYUI_LAZY))
         FormVerify.verifyItem(ele);
       let vDom = binding.modifiers.vDom;
       let pDom = binding.modifiers.pDom;
       let VM = binding.modifiers.vm;
       let attr = binding.modifiers.attr;
       // 这里重新去渲染layui的表单, 父表单的filter尝试从虚拟属性里面去获取
       layui.form.render("checkbox", attr.filter);
       // ele的落地是在属性解析之后，所以可以通过这个判断是否第一次解析
       if (vDom.ele) return;
       // 接下来需要在切换checkbox时修改源数值
       ele.addEventListener("change", function () {
         // 这里需要重新获取值，否则不能达到修改源数据的效果
         let _data = VM.proxy.parseAttr.call(VM, attr, vDom);
         if (this.checked) {
           if (_.indexOf(_data, value) < 0) _data.push(value);
         } else {
           if (_.indexOf(_data, value) >= 0)
             _data.splice(_.indexOf(_data, value), 1);
         }
       });
       // layui还要增加一条，监听它的form修改事件
       LayuiFormOn.checkboxProxy(ele, vDom, pDom.ele, attr, VM);
     },
 
     /**
      * 单选框的绑定渲染
      * @param {*} ele
      * @param {*} binding
      * @returns
      */
     renderRadio(ele, binding) {
       // 获取它的value 和checked
       let value = ele.value;
       let checked = ele.checked;
       // 判断获取的值里面有没得这一项
       let data = binding.value;
       // 这里就切换了checkbox
       ele.checked = data == value;
       // 判断是否需要检查当前的这个表单元素
       if (false !== ele.getAttribute(constant.LAYUI_LAZY))
         FormVerify.verifyItem(ele);
       let VM = binding.modifiers.vm;
       let vDom = binding.modifiers.vDom;
       let attr = binding.modifiers.attr;
       // ele的落地是在属性解析之后，所以可以通过这个判断是否第一次解析
       if (vDom.ele) {
         // 这里重新去渲染layui的表单, 父表单的filter尝试从虚拟属性里面去获取
         layui.form.render("radio", attr.filter);
         return;
       }
       // 如果当前的值为空，但是这个单选框选中，就将这个单选框的值赋给绑定的变量值
       if (!data && checked) {
         FormRender.updateData(vDom, binding.expression, value, VM);
         data = value;
         ele.checked = data == value;
         // 这里修改了表单的值也需要重新渲染
         layui.form.render("radio", attr.filter);
       }
 
       // 接下来需要在切换radio时修改源数值
       ele.addEventListener("change", function () {
         if (this.checked)
           FormRender.updateData(vDom, binding.expression, value, VM);
       });
       // layui还要增加一条，监听它的form修改事件
       LayuiFormOn.radioProxy(ele, vDom, pDom.ele, attr, VM);
     },
 
     /**
      * 下拉框的绑定渲染
      * @param {*} ele
      * @param {*} binding
      * @returns
      */
     renderSelect(ele, binding) {
       // 赋值
       ele.value = binding.value;
       // 判断是否需要检查当前的这个表单元素
       if (false !== ele.getAttribute(constant.LAYUI_LAZY))
         FormVerify.verifyItem(ele);
       let vDom = binding.modifiers.vDom;
       let pDom = binding.modifiers.pDom;
       let attr = binding.modifiers.attr;
       // ele的落地是在属性解析之后，所以可以通过这个判断是否第一次解析
       if (vDom.ele) {
         // 这里重新去渲染layui的表单, 父表单的filter尝试从虚拟属性里面去获取
         layui.form.render("select", attr.filter);
         return;
       }
       let VM = binding.modifiers.vm;
       // 第一次的时候要将它下面的option设置对，循环渲染的不管
       // 这里使用直接删除虚拟属性的方式是因为，到现在，子节点option还没有开始解析
       let flag = false;
       _.each(vDom.children, (v) => {
         if (v.tagName.toLowerCase() == "option") {
           let _value = util.getVDomAttr(v, "value");
           if (binding.value == _value) {
             // 选中,给这个添加一个 selected 的属性
             if (!v.attrs["selected"]) v.attrs["selected"] = new vAttr(true);
             flag = true;
           } else {
             if (v.attrs["selected"]) delete v.attrs["selected"];
           }
         }
       });
       // 如果当前值没有匹配上任何一个选项值，就取第一个来赋值
       if (!flag && vDom.children.length > 0) {
         let firstValue = util.getVDomAttr(vDom.children[0], "value");
         FormRender.updateData(vDom, binding.expression, firstValue, VM);
         // 重新对下拉框进行赋值
         ele.value = firstValue;
       }
       // 这里修改了表单的值也需要重新渲染
       layui.form.render("select", attr.filter);
       // 接下来需要在切换radio时修改源数值
       ele.addEventListener("change", function () {
         FormRender.updateData(vDom, binding.expression, this.value, VM);
       });
       // layui还要增加一条，监听它的form修改事件
       LayuiFormOn.selectProxy(ele, vDom, pDom.ele, attr, VM);
     },
 
     renderInput(ele, binding) {
       // 赋值
       ele.value = binding.value;
       // 判断是否需要检查当前的这个表单元素
       if (false !== ele.getAttribute(constant.LAYUI_LAZY))
         FormVerify.verifyItem(ele);
       // 判断是否需要进行表单校验  lay-lazy 设置为false不需要校验，直接跳过
       let vDom = binding.modifiers.vDom;
       // ele的落地是在属性解析之后，所以可以通过这个判断是否第一次解析
       if (vDom.ele) return;
       let VM = binding.modifiers.vm;
       // 检查是否有lay-lazy属性来判断添加什么样的监听事件
       let eventKey = vDom.attrs[constant.LAYUI_LAZY]
         ? "blur"
         : "input onpropertychange";
       // 接下来需要在切换radio时修改源数值
       _.each(eventKey.split(" "), (v) => {
         ele.addEventListener(v, function () {
           FormRender.updateData(vDom, binding.expression, this.value, VM);
         });
       });
     },
   };
 
   /**
    * @namespace LayuiFormOn
    * @desc 处理layui表单相关，layui.form会去渲染页面，
    *    要想达到准确的效果，不仅要去监听表单元素的change事件
    *    还应该去监听layui.form提供的on事件。这里会占用这个事件
    */
   var LayuiFormOn = {
     /**
      * @method 新增一个layui.from的监听事件，监听layui表单的变化
      * @param {*} type    表单类型
      * @param {*} filter  表单过滤器
      * @param {*} cb      回调函数
      */
     addLayuiFormListener(type, filter, cb) {
       layui.form.on(type + "(" + filter + ")", () => {
         cb && cb.apply();
       });
     },
 
     /**
      * @method 多选框添加layui的表单切换与数据之间的绑定
      * @desc
      *    这里没有传入虚拟节点和虚拟属性，是根据key进行绑定的
      * @param {*} item            添加事件的表单元素
      * @param {*} formItem        表单元素的父元素，通过父元素要查找同名元素
      * @param {*} expression      对应vm属性值的key
      * @param {*} vm              视图对象
      */
     checkboxNomal(item, formItem, expression, vm) {
       LayuiFormOn.addLayuiFormListener(
         "checkbox",
         util.getFormFilterName(item),
         function () {
           // 遍历表单下面的每一个同名的多选项
           formItem
             .querySelectorAll('[name="' + item.name + '"]')
             .forEach((checkbox) => {
               // 首先获取对应的多项数据值，每次都要重新获取
               let _data = vm.proxy.getValue.call(vm, expression);
               // 如果元素被选中就添加上这一项，否则移除这一项
               if (checkbox.checked) {
                 if (_.indexOf(_data, checkbox.value) < 0)
                   _data.push(checkbox.value);
               } else {
                 if (_.indexOf(_data, checkbox.value) >= 0)
                   _data.splice(_.indexOf(_data, checkbox.value), 1);
               }
             });
         }
       );
     },
 
     /**
      * @method 多选框添加layui的表单切换与数据之间的绑定(通过虚拟属性节点)
      * @desc
      *     这里传入虚拟节点和虚拟属性，可以根据他们的具体情况动态的绑定
      * @param {*} item          添加事件的表单元素
      * @param {*} vDom          添加事件的虚拟节点
      * @param {*} formItem      表单元素的父元素，通过父元素要查找同名元素
      * @param {*} attr          虚拟属性节点
      * @param {*} vm            视图对象
      */
     checkboxProxy(item, vDom, formItem, attr, vm) {
       LayuiFormOn.addLayuiFormListener(
         "checkbox",
         util.getFormFilterName(item),
         function () {
           // 遍历表单下面的每一个同名的多选项
           formItem
             .querySelectorAll('[name="' + item.name + '"]')
             .forEach((checkbox) => {
               // 首先获取对应的多项数据值，每次都要重新获取
               let _data = vm.proxy.parseAttr.call(vm, attr, vDom);
               // 如果元素被选中就添加上这一项，否则移除这一项
               if (checkbox.checked) {
                 if (_.indexOf(_data, checkbox.value) < 0)
                   _data.push(checkbox.value);
               } else {
                 if (_.indexOf(_data, checkbox.value) >= 0)
                   _data.splice(_.indexOf(_data, checkbox.value), 1);
               }
             });
         }
       );
     },
 
     /**
      * @method 单选框添加layui的表单切换与数据之间的绑定
      * @desc
      *    这里没有传入虚拟节点和虚拟属性，是根据key进行绑定的
      * @param {*} item          添加事件的表单元素
      * @param {*} formItem      表单元素的父元素，通过父元素要查找同名元素
      * @param {*} expression    对应vm属性值的key
      * @param {*} vm            视图对象
      */
     radioNomal(item, formItem, expression, vm) {
       LayuiFormOn.addLayuiFormListener(
         "radio",
         util.getFormFilterName(item),
         function () {
           // 遍历表单下面的每一个同名的单选项
           formItem
             .querySelectorAll('[name="' + item.name + '"]')
             .forEach((radio) => {
               // 如果元素被选中就直接修改对应的值
               if (radio.checked)
                 vm.proxy.getValue.call(vm, expression, radio.value);
             });
         }
       );
     },
 
     /**
      * @method 单选框添加layui的表单切换与数据之间的绑定(通过虚拟属性节点)
      * @desc
      *     这里传入虚拟节点和虚拟属性，可以根据他们的具体情况动态的绑定
      * @param {*} item          添加事件的表单元素
      * @param {*} vDom          添加事件的虚拟节点
      * @param {*} formItem      表单元素的父元素，通过父元素要查找同名元素
      * @param {*} attr          虚拟属性节点
      * @param {*} vm            视图对象
      */
     radioProxy(item, vDom, formItem, attr, vm) {
       LayuiFormOn.addLayuiFormListener(
         "radio",
         util.getFormFilterName(item),
         function () {
           // 遍历表单下面的每一个同名的单选项
           formItem
             .querySelectorAll('[name="' + item.name + '"]')
             .forEach((radio) => {
               // 如果元素被选中就直接修改对应的值
               if (radio.checked)
                 FormRender.updateData(vDom, attr.expression, radio.value, vm);
             });
         }
       );
     },
 
     /**
      * @method 下拉框添加layui的表单切换与数据之间的绑定
      * @desc
      *    这里没有传入虚拟节点和虚拟属性，是根据key进行绑定的
      * @param {*} item          添加事件的表单元素
      * @param {*} expression    对应vm属性值的key
      * @param {*} vm            视图对象
      */
     selectNomal(item, expression, vm) {
       LayuiFormOn.addLayuiFormListener(
         "select",
         util.getFormFilterName(item),
         function () {
           // 直接修改对应的值
           vm.proxy.getValue.call(vm, expression, item.value);
         }
       );
     },
 
     /**
      * @method 下拉框添加layui的表单切换与数据之间的绑定(通过虚拟属性节点)
      * @param {*} item          添加事件的表单元素
      * @param {*} vDom          添加事件的虚拟节点
      * @param {*} formItem      表单元素的父元素，通过父元素要查找同名元素
      * @param {*} attr          虚拟属性节点
      * @param {*} vm            视图对象
      */
     selectProxy(item, vDom, formItem, attr, vm) {
       LayuiFormOn.addLayuiFormListener(
         "select",
         util.getFormFilterName(item),
         function () {
           // 直接修改对应的值
           FormRender.updateData(vDom, attr.expression, item.value, vm);
         }
       );
     },
   };
 
   let formProxy = {
     wakeUp: function () {
       // 已经初始化过了就不用再执行下面的方法了
       if (formProxy.notify()) return false;
       // 在form下面防置一个空间变量，用来保存后面生成的视图对象
       if (!layui.form.$children) layui.form.$children = [];
       /**
        * 拦截render函数
        * @param {*} type    待渲染组件的类型
        * @param {*} filter  待渲染组件的form的filter
        */
       layui.form.render = function (type, filter) {
         // 获取表单选择器
         let selector = filter
           ? `.${constant.LAYUI_FORM}[${constant.LAYUI_FILTER}="${filter}"]`
           : `.${constant.LAYUI_FORM}`;
         let res = null;
         document.querySelectorAll(selector).forEach((ele) => {
           if (ele instanceof HTMLElement) {
             // 1. 获取当前表单的lay-filter 如果没有就自动生成一个，这个必须有，是作为表单的name存在的
             let name = util.getFormName(ele);
             // 2. 获取视图对象赋值给返回值
             res = util.getFormByName(name) || formProxy.render(ele);
             // 最后调用原生的render方法渲染表单
             renderForever.call(layui.form, type, filter);
           }
         });
         return res;
       };
     },
 
     notify: function () {
       return !!layui.form.$children;
     },
 
     /**
      * @method 处理layui的form创建并返回视图对象
      * @param {*} item 待处理的form表单
      */
     render: function (item) {
       // 1.获取form的名字
       let name = util.getFormName(item);
       // 2.创建一个对象，用来描述整个表单的属性值
       let data = {};
       data[constant.LAYUI_FORM_STR] = {};
       // 3.创建一个视图对象
       let vm = layui.binder({
         data: data, // 将表单数据对象放入
         methods: {
           /**
            * @method 根据传入的dom，添加一个监视属性，当然如果没有这个属性就现添加一个
            * @param {*} formItem
            */
           watchHtmlElement(formItem) {
             let self = this;
             // 获取元素的name属性，作为它的key
             let _name = formItem.name;
             // 由于表单可能由layui渲染过了，生成了一些干扰表单元素， 有点干扰，这里尽量避免干扰
             // 这里就确定了，没得name属性的不进行渲染
             if (!_name) return;
             // 获取元素的type属性，checkbox对应的是数组，其它的就直接是值
             let _type =
               formItem.type == "checkbox"
                 ? "checkbox"
                 : formItem.type == "radio"
                 ? "radio"
                 : formItem.tagName.toLowerCase() == "select"
                 ? "select"
                 : "input";
             // 判断当前是否有以这个name为key的值，如果没有就添加一个
             if (!self.$data[constant.LAYUI_FORM_STR][_name])
               self.$set(
                 self.$data[constant.LAYUI_FORM_STR],
                 _name,
                 "checkbox" == _type ? [] : ""
               );
             /**
              * 1.初始赋值，将页面上的值赋给现在的这个变量
              * 2.添加添加dom的change事件监听
              */
             if ("checkbox" == _type) {
               item
                 .querySelectorAll('[name="' + _name + '"]')
                 .forEach((checkbox) => {
                   if (checkbox.checked)
                     self.$data[constant.LAYUI_FORM_STR][_name].push(
                       checkbox.value
                     );
                 });
               /**
                * 由于页面会被layui美化过，使用dom自带的change事件似乎不太行，
                * 这里使用layui提供的on事件进行监听，但是前提是需要对当前表单元素添加上了lay-filter属性
                * 如果没有lay-filter属性，这里为它根据name自动添加一个filter属性，保证可以正常的使用
                *
                * 由于checkbox或者radio可能触发多次绑定，但是layui事件管理都是一对一的不会重复
                */
               LayuiFormOn.checkboxNomal(
                 formItem,
                 item,
                 constant.LAYUI_FORM_STR + "." + _name,
                 self
               );
             } else if ("radio" == _type) {
               let value = "";
               item
                 .querySelectorAll('[name="' + _name + '"]')
                 .forEach((radio) => {
                   if (radio.checked) value = radio.value;
                 });
               self.$data[constant.LAYUI_FORM_STR][_name] = value;
               /**
                * 由于页面会被layui美化过，使用dom自带的change事件似乎不太行，
                * 这里使用layui提供的on事件进行监听，但是前提是需要对当前表单元素添加上了lay-filter属性
                * 如果没有lay-filter属性，这里为它根据name自动添加一个filter属性，保证可以正常的使用
                *
                * 由于checkbox或者radio可能触发多次绑定，但是layui事件管理都是一对一的不会重复
                */
               LayuiFormOn.radioNomal(
                 formItem,
                 item,
                 constant.LAYUI_FORM_STR + "." + _name,
                 self
               );
             } else if ("select" == _type) {
               self.$data[constant.LAYUI_FORM_STR][_name] = formItem.value;
               /**
                * 由于页面会被layui美化过，使用dom自带的change事件似乎不太行，
                * 这里使用layui提供的on事件进行监听，但是前提是需要对当前表单元素添加上了lay-filter属性
                * 如果没有lay-filter属性，这里为它根据name自动添加一个filter属性，保证可以正常的使用
                */
               LayuiFormOn.selectNomal(
                 formItem,
                 constant.LAYUI_FORM_STR + "." + _name,
                 self
               );
             } else {
               let eventKey =
                 formItem.getAttribute(constant.LAYUI_LAZY) === undefined ||
                 formItem.getAttribute(constant.LAYUI_LAZY) === null
                   ? "blur"
                   : "input onpropertychange";
               // input 和textarea 不考虑页面美化带来的影响
               _.each(eventKey.split(" "), (v) => {
                 formItem.addEventListener(v, function () {
                   self.proxy.getValue.call(
                     self,
                     constant.LAYUI_FORM_STR + "." + _name,
                     this.value
                   );
                 });
               });
             }
 
             // 3.添加监视属性
             self.$watch(constant.LAYUI_FORM_STR + "." + _name, function (v) {
               // 当属性值修改时，表单值也需要发生改变
               if ("checkbox" == _type) {
                 item
                   .querySelectorAll('[name="' + _name + '"]')
                   .forEach((checkbox) => {
                     if (_.indexOf(v, checkbox.value) >= 0) {
                       checkbox.checked = true;
                       checkbox.setAttribute("checked", "checked");
                     } else {
                       checkbox.checked = false;
                       checkbox.removeAttribute("checked");
                     }
                   });
               } else if ("radio" == _type) {
                 item
                   .querySelectorAll('[name="' + _name + '"]')
                   .forEach((radio) => {
                     if (v == radio.value) {
                       radio.checked = true;
                       radio.setAttribute("checked", "checked");
                     } else {
                       radio.checked = false;
                       radio.removeAttribute("checked");
                     }
                   });
               } else if ("select" == _type) {
                 formItem.value = v;
                 formItem.querySelectorAll("option").forEach((option) => {
                   if (v == option.value) {
                     option.setAttribute("selected", "selected");
                   } else {
                     option.removeAttribute("selected");
                   }
                 });
               } else {
                 formItem.value = v;
               }
               // 最后必不可少的就是要重新渲染这个表单了
               if ("input" !== _type) layui.form.render(_type, self.$name);
               // 表单校验
               let lazy = formItem.getAttribute(constant.LAYUI_LAZY);
               if (false === lazy) self.verifyItem(_name);
             });
           },
 
           /**
            * @public
            * @method 检查表单元素填写的值是否正确
            * @param {*} item 待检查的表单元素
            * @returns
            */
           verifyItem(item) {
             if (item instanceof HTMLElement)
               return FormVerify.verifyItem.call(this, item);
             // 也可以是string
             if (!_.isString(item)) return false;
             let self = this,
               res = true;
             self.$ele
               .querySelectorAll('[name="' + item + '"]')
               .forEach((dom) => {
                 if (!FormVerify.verifyItem(dom)) res = false;
               });
             return res;
           },
 
           /**
            * @public
            * @method 检查一个表单内所有元素是否全部填写正确
            * @returns
            */
           verifyForm() {
             return FormVerify.verifyForm.call(this, this.$name);
           },
 
           /**
            * @public
            * @method 检验一个表单并返回表单值
            * @returns
            */
           getFormValue() {
             return FormVerify.getFormValue.call(this, this.$name);
           },
         },
         created() {
           /**
            * 由于这个对象不用挂载dom,所以在创建完毕的回调里面添加逻辑
            *
            * 1.从数据到dom，这里需要通过监视属性来实现
            * 2.从dom到数据，这里通过添加dom的监听事件来实现(由于layui会对表单进行美化，这里需要抢占layui的监听事件)
            */
           let self = this;
           // 将表单的name和dom带上
           self.$name = name;
           self.$ele = item;
           // 首先遍历表单元素
           let fieldElem = $(item).find("input,select,textarea");
           _.each(fieldElem, (ele) => {
             if (ele instanceof HTMLElement) {
               self.watchHtmlElement(ele);
             }
           });
         },
       });
       // 放入form中
       layui.form.$children.push(vm);
       // 返回这个新建的视图
       return vm;
     },
   };
   let handler = {
     wakeUp: function () {
       return formProxy.wakeUp();
     },
 
     notify: function () {
       return formProxy.notify();
     },
 
     verifyItem: function (item) {
       return FormVerify.verifyItem(item);
     },
 
     verifyForm: function (filter) {
       return FormVerify.verifyForm(filter);
     },
 
     getFormValue: function (filter) {
       return FormVerify.getFormValue(filter);
     },
   };
 
   layui.binder.diretive("form", function (ele, binding) {
     let vDom = binding.modifiers.vDom;
     let attr = binding.modifiers.attr;
     // 判断是否是第一次
     let first = !attr.first;
     if (!first) return;
     attr.first = true;
     // 获取表单的filter属性,如果没有需要自动添加
     let formfilter = util.getFormName(ele);
     let vm = binding.modifiers.vm;
     util.findChildren(vDom.children, function (_vDom) {
       const _tagName = _vDom.tagName.toLowerCase();
       const _flag =
         _tagName == "input" || _tagName == "select" || _tagName == "textarea";
       if (_flag) {
         // 添加一个layitem指令(属性,属性值就是它的name的值,没有name就不添加)
         const _name = util.getVDomAttr(_vDom, "name");
         // 由于这里不能添加属性，干脆将name属性直接赋值，反正都差不多
         if (_name) {
           _vDom.attrs["v-layitem"] = _vDom.attrs["name"];
           // 将表单的filter属性放入attr中，到时候方便表单元素添加监听事件
           _vDom.attrs["v-layitem"].filter = formfilter;
           // 拼接表达式
           const formStr = attr.expression;
           _vDom.attrs["v-layitem"].expression = formStr + '.' + _name;
           // 自动补全表单所需项
           let _type = util.getVDomAttr(_vDom, "type");
           // if (!vm.$data[formStr][_name])
           //   vm.$set(vm.$data[formStr], _name, "checkbox" == _type ? [] : "");
           if(binding.value[_name] === undefined)
             vm.$set(binding.value, _name, "checkbox" == _type ? [] : "");
         }
       }
     });
   });
 
   layui.binder.diretive("layitem", function (ele, binding) {
     let vDom = binding.modifiers.vDom;
     let attr = binding.modifiers.attr;
     // 首先要判断它是哪一种表单元素然后根据条件作出响应的处理
     // TODO 下面的方式部分也是照搬binder.js里面的内容，抽空想想怎么优化
     let type = util.getVDomAttr(vDom, "type");
     if ("checkbox" == type) {
       // 多选框的渲染
       FormRender.renderCheckBox(ele, binding);
       return;
     }
     if ("radio" == type) {
       // 单选框的渲染
       FormRender.renderRadio(ele, binding);
       return;
     }
     if ("select" == vDom.tagName.toLowerCase()) {
       // 下拉框的渲染
       FormRender.renderSelect(ele, binding);
       return;
     }
     // 其它的是普通的input框
     FormRender.renderInput(ele, binding);
   });
 
   layui.binder.diretive("laysource", function (ele, binding) {
     let vDom = binding.modifiers.vDom;
     let vm = binding.modifiers.vm;
     let attr = binding.modifiers.attr;
     // 下拉框的source指带的是它下面的option项
     let type = util.getVDomAttr(vDom, "type");
     if ("select" == vDom.tagName.toLowerCase()) {
 
       // 去重防止重复触发，这里有赋值操作，防止select取值改变从而引起的表单重新渲染
       if(attr.source == binding.value) return;
       attr.source = binding.value
 
       // 首先需要拼接默认项 lay-option
       let defaultOption = util.getVDomAttr(vDom, "lay-option");
       let htmlTemplate = defaultOption
         ? `<option value = ''>${defaultOption}</option>`
         : "";
       _.each(
         binding.value,
         (v) => (htmlTemplate += `<option value = '${v.id}'>${v.name}</option>`)
       );
       $(ele).empty().append($(htmlTemplate));
       // js 赋值触发后面的变化
       // 获取绑定表达式
       let bindAttr = vDom.attrs['v-layitem'];
       let firstValue = defaultOption ? '' : binding.value.length > 0 ? binding.value[0].id : '';
       if(bindAttr) FormRender.updateData(vDom, bindAttr.expression, firstValue, vm);
       // setTimeout(function(){
       //   if(ele.value)
       //     $(ele).siblings("div.layui-form-select").find('dl dd[lay-value=' + ele.value + ']').click();
       // }, 500)
       return;
     }
   });
 
   layui.form.wakeUp = function () {
     handler.wakeUp();
   };
 
   exports("vform", handler);
 });
 